Return to the table of contents


Stringing Along

You may recall the discussion near the beginning of Chapter inventor.htm of native vs. user defined variable types. I provided a list of native C++ variable types; namely, char, short, long, float, double, bool, and int. We've already created several classes for our inventory control project, and now it's time to apply what we've learned to a more basic type, the string. We've been using strings for a long time, and now it's time to see exactly how to implement the string class we've been using.

Objectives of This Chapter

By the end of this chapter, you should

  1. Understand how variables of the string class are created, destroyed, and assigned to one another.
  2. Understand how to assign memory to variables where the amount of memory needed is not known until the program is running.
  3. Understand how literal C strings can be used to initialize variables of the string class.

Playing out the string

Susan had some questions about these objectives. Here's the discussion.

Susan: What is the difference between literal C strings and variables of the string class?

Steve: A variable of the string class is what you've been using to store variable-length alphanumeric data. You can copy them, input them from the keyboard, assign values to them, and the like. By contrast, a literal C string is just a bunch of characters in a row; all you can do with it is display it or assign it to a string variable.

Susan: OK, then you are saying that variables of the string class are what I am used to working with, but I just didn't know that they were part of a class because you were keeping this all a big secret. On the other hand, a literal C string is just some nonsense that you want me to learn to assign to something that might make sense? OK, this is great; sure, this is logical<?>. Hey, a literal C string must be a part of the native language?

Steve: Right all the way along.

Susan: Yes, but why would something so basic as string not be part of the native language? This is what I don't understand. And vectors too; even though they are gross, I can see that they are a very necessary evil. So tell me why those basic things would not be part of the native language?

Steve: That's a very good question. In fact, both strings and vectors are going to be supplied by the "standard library" that will be part of the C++ standard, which should take effect about the time this book comes out (mid-1996). They still won't be part of the language itself, because that would make the compiler even more complicated than it already is. However, the user will still be able to use them without having to make them up, so the fact that they aren't part of the language will be of mostly theoretical interest.

Before we get into how to create the string class we've been using in this book, I should expand on the answer I gave Susan as to why string isn't a native type in the first place: To keep the C++ language itself as simple as possible. One of the design goals of C++, as of C, was to allow the language to be moved, or ported, from one machine type to another as easily as possible. Since strings, vectors, and so on can be written in C++ (i.e., created out of the more elementary parts of the language), they don't have to be built in. This reduces the amount of effort needed to port C++. In addition, some applications don't need and can't afford anything but the barest essentials; "embedded" CPUs such as those in cameras, VCRs, elevators, or microwave ovens, are probably the most important users of such applications, and such devices are much more common than "real" computers. However, this still leaves the question of why these data types haven't been in a standard library that can be used when necessary. Bjarne says in his book Design and Evolution of C++ that it might very well have been a mistake to have released C++ to the general public without this library; I tend to agree. In any event, this situation will be remedied when the C++ standard is adopted, which as mentioned is scheduled to occur sometime next year (1998).

Even though strings aren't native, we've been using them for some time already without realizing that they're not native variables, so it should be fairly obvious that this class has to provide the facilities of a concrete data type; that is, one whose objects can be created, copied, assigned, and destroyed as though they were native variables. You may recall from the discussion starting on page in Chapter inventor.htm that such data types need a default constructor, a copy constructor, an assignment operator, and a destructor. To refresh your memory, here's the description of each of these member functions:

  1. A default constructor creates an object when there is no initial value specified for the object.
  2. A copy constructor makes a new object with the same contents as an existing object of the same type.
  3. An assignment operator sets an existing object to the value of another object of the same type.
  4. A destructor cleans up when an object expires; for a local object, this occurs at the end of the block where it was created.

In our StockItem and Inventory class definitions, the compiler-generated versions of these functions were fine for all but the default constructor. In the case of the string class, though, we're going to have to create our own versions of all four of these functions, for reasons that will become apparent as we examine their implementations in this chapter and the next one. Before we can implement these member functions for our string class, though, we have to define exactly what a string is. The string class is a data type that gives us the following capabilities in addition to those facilities that every concrete data type provides:

  1. We can set a string to a literal value like "abc".
  2. We can display a string on the screen with the << operator.
  3. We can read a string in from the keyboard with the >> operator.
  4. We can compare two strings to find out whether they are equal.
  5. We can compare two strings to find out which is "less than" the other; that is, which one would come first in the dictionary.

We'll see how all of these capabilities work sometime in this chapter or the next one, but for now let's start with Figure stringinterface1, a simplified version of the interface specification for the string class, which includes the specification of the four member functions needed for a concrete data type, as well as a special constructor that is specific to the string class. I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter; those files are string1.h, string1.cc, and strtst1.cc, respectively.

The string class interface, initial version (code\string1.h) (Figure stringinterface1)

code/string1.h

The first four member functions in that interface are the standard concrete data type functions. In order, they are

  1. The default constructor
  2. The copy constructor
  3. The assignment operator, operator =
  4. The destructor

I've been instructed by Susan to let you see all of the code that implements this initial version of our string class at once before we start to analyze it. Of course, I've done so, and Figure stringimpl is the result.

The initial implementation for the string class (code\string1.cc) (Figure stringimpl)

code/string1.cc

Now let's start by looking at the default constructor. Figure stringdefaultcons shows its implementation.

The default constructor for the string class (from code\string1.cc) (Figure stringdefaultcons)

string::string()

: m_Length(1),

m_Data(new char [m_Length])

{

memcpy(m_Data,"",m_Length);

}

The member initialization list in this constructor contains two expressions. The first of them, m_Length(1), isn't very complicated at all. It simply sets the length of our new string to 1. However, this may seem a bit odd: Why do we need any characters at all for a string that has no value? The answer to this riddle is quite simple: To make our strings as compatible as possible with pre-existing C functions that work on C strings, we need to include the null byte that terminates all C strings, so we need to reserve one more byte of memory for a string's data than are needed to hold the characters that we actually want to store. In the current case of a zero-character string, this means that we need one byte of storage for the null byte.

It's important to note that this is an example where the order of execution of member initializer expressions is important: we definitely want m_Length to be initialized before m_Data, because the amount of data being assigned to m_Data depends on the value of m_Length. As you may remember from Chapter inventor.htm, the order in which the initialization expressions are executed is dependent not on the order in which they are written in the list, but on the order in which the member variables being initialized are declared in the class interface definition. Therefore, it's important to make sure that the order in which those member variables are declared is correct. In this case, it is, because m_Length is declared before m_Data in string1.h.

Before proceeding to the next member initialization expression, let's take a look at the characteristics of the variables that we're using here. The scope of these variables, as we know from our previous discussion of the StockItem class, is class scope; therefore, each object of the string class has its own set of these variables, and they are accessible from any member functions of the class as though they were global variables.

However, an equally important characteristic of each of these variables is its data type. The type of m_Length is short, which is a type we've encountered before, a 16-bit integer variable that can hold a number between -32768 and 32767. But what about the type of the other member variable, m_Data, which is listed in Figure stringinterface1 as char*? We know what a char is, but what does that * mean?

Passing Along a Few Pointers

The star means pointer, which is just another term for a memory address. In particular, char* (pronounced "char star") means "pointer to a char".1 This is considered one of the most difficult concepts for beginning programmers to grasp, but you shouldn't have any trouble understanding its definition if you've been following the discussion so far: A pointer is the address of some data item in memory. That is, to say "a variable points to a memory location" is almost exactly the same as saying "a variable's value is the address of a memory location". In the specific case of a variable x of type char*, for example, to say "x points to a C string" is exactly the same as saying "x contains the address of the first byte of the C string."2 The m_Data variable is used to hold the address of the first char of the data that a string contains; the rest of the characters follow the first character at consecutively higher locations in memory.

If this sounds familiar, it should; a literal C string like "hello" (Chapter basics.htm) consists of a number of chars in consecutive memory locations; it should come as no surprise then when I tell you that a literal C string has the type char*.

As you might infer from these cases, our use of one char* to refer to multiple chars isn't an isolated example. Actually, it's quite a widespread practice in C++, which brings up an important point: A char*, or any other type of pointer for that matter, has two different possible meanings in C++.3 One of these meanings is the obvious one of signifying the address of a single item of the type the pointer points to. In the case of a char*, that means the address of a char. However, in the case of a literal C string, as well in the case of our m_Data member variable, we use a char* to indicate the address of the first char of an indeterminate number of chars; any chars after the first one occupy consecutively higher addresses in memory. Most of the time, this distinction has little effect on the way we write programs, but sometimes we have to be sensitive to this "multiple personality" of pointers; we'll run across one of these cases later in this chapter.

Susan had some questions (and I had some answers) on this topic of the true meaning of a char*:

Susan: What I get from this is that char* points to a char address either singularly or as the beginning of a string of multiple addresses. Is that right?

Steve: Yes, except that it's a string of several characters, not addresses.

Susan: Oh, here we go again; this is so confusing. So if I use a string "my name is" then char* points to the address that holds the string of all those letters. But if the number of letters exceeds what the address can hold, won't it take up the next available address in memory and char* point to it after it points to the first address?

Steve: Each memory address can hold 1 byte; in the case of a string, that byte is the value of one char of the string's data. So a char*, as we use it, will always point to the first char of the chars that hold our string's value; the other chars follow that one immediately in memory.

Susan: Let me ask this: When you show an example of a string with the value "Test" (Figure strnmem), the pointer at address 12340002 containing the address 1234febc is really pointing at the T as that would be the first char and the rest of the letters will actually be in the other immediately following bytes of memory?

Steve: Absolutely correct.

While we're on the subject of that earlier discussion of literal C strings, you may recall that I bemoaned the fact that such literal C strings use a 0 byte to mark the end of the literal value, rather than keeping track of the length separately. Nothing can be done about that decision now, at least as it applies to literal C strings. In the case of the string class, however, the implementation is under our control rather than the language designer's; therefore, I've decided to use a length variable (m_Length) along with the variable that holds the address of the first char of the data (m_Data).

In other words, what we're doing in this exercise is synthesizing a new data type called string. A string needs a length and a set of characters to represent the actual data in the string. The short named m_Length is used in the string class to keep track of the number of characters in the data part of the string; the char* named m_Data is used to hold the address of the first character of the data part of the string.

The next member initialization expression, m_Data(new char [m_Length]), takes us on another of our side trips. This one has to do with the dreaded topic of dynamic memory allocation.

The Dynamic Duo, new and delete

So far, we've encountered two storage classes: static and auto. As you might recall from the discussion in Chapter function.htm, static variables are allocated memory when the program is linked, while the memory for auto variables is assigned to them at entry to the block where they are defined. However, both mechanisms have a major limitation: The amount of memory needed is fixed when the program is compiled. In the case of a string, we need to allocate an amount of memory that cannot be known until the program is executed, so we need another storage class.

As you will be happy to learn, there is indeed another storage class called dynamic storage that enables us to decide the amount of memory to allocate at run time.4 To allocate memory dynamically, we use the new operator, specifying the data type of the memory to be allocated and the number of elements that we need. In our example member initialization expression, m_Data(new char [m_Length]), the type is char and the count is m_Length. The result of calling new is a pointer to the specified data type; in this case, since we want to store chars, the result of calling new is a pointer to a char, that is, a char*. This is a good thing, because char* is the type of the variable m_Data that we're initializing to the address that is returned from new. So the result of the member initialization expression we're examining is to set m_Data to the value returned from calling new; that value is the address of a newly assigned block of memory that can hold m_Length chars. In the case of the default constructor, we've asked for a block of 1 byte, which is just what we need to hold the contents of the zero-length C string that represents the value of our empty string.

It may not be obvious why we need to call new to get the address where we will store our data. Doesn't a char* always point to a byte in memory? Yes, it does; the problem is which byte. We can't use static (link time) or auto (function entry time) allocation for our string class, because each string can have a different number of characters. Therefore, we have to assign the memory after we find out how many characters we need to store the value of the string. The new operator reserves some memory and returns the address of the beginning of that memory. In this case, we assign that address to our char* variable called m_Data. An important point to note here is that in addition to giving us the address of a section of memory, new also gives us the right to use that memory for our own purposes. That same memory area will not be made available for any other use until we say we're done with it by calling another operator called delete. Susan had some questions about how (and why) we use new. Here's the discussion:

Susan: OK, so all Figure stringdefaultcons does is lay the foundation to be able to acquire memory to store the C string "" and then copy that information that will go into m_Data that starts at a certain location in memory?

Steve: Right; Figure stringcharstarcons is the code for the constructor that accomplishes that task.

Susan: When you say that "the amount of memory needed is fixed when the program is compiled" that bothers me. I don't understand that in terms of auto variables, or is this because that type is known such as a short?

Steve: Right. As long as the types and the quantity of the data items in a class definition are known at compile time, as is the case with auto and static variables, the compiler can figure out the amount of memory they need. The addresses of auto variables aren't known at compile time, but how much space they will take up is.

Susan: OK, I understand the types of the data items. However, I am not sure what you mean by the quantity; can you give me an example?

Steve: Sure. You might have three chars and four shorts in a particular class definition; in that case, the compiler would add up three times the length of a char and four times the length of a short and allocate that much memory (more or less). Actually, some other considerations affect the size of a class object that aren't relevant to the discussion here, but they can all be handled at compile time and therefore still allow the compiler to figure out the amount of memory needed to store an object of any class.

The statement inside the {} in the default constructor, memcpy(m_Data,"",m_Length); is responsible for copying the null byte from the C string "" to our newly allocated area of memory. The function memcpy (short for "memory copy") is one of the C standard library functions for C string and memory manipulation. As you can see, it takes three arguments. The first argument is a pointer to the destination, that is, the address that will receive the data. The second argument is a pointer to the source of the data; this, of course, is the address that we're copying from (i.e., the address of the null byte in the "", in our example). The last argument is the number of bytes to copy.

In other words, memcpy reads the bytes that starts at the address specified by its input argument (in this case, "") and writes a copy of those bytes to addresses starting at the address specified by its output argument (in this case, m_Data). The amount copied is specified by the length argument (in this case, m_Length). Effectively, therefore, memcpy copies a certain amount of data from one place in memory to another; in this case, it copies 1 byte from the address of the literal C string "" to the address pointed to by m_Data (that is, the place where we're storing the data that make up the value of our string).

This notion of dynamic allocation was the subject of some more discussion with Susan.

Susan: This stuff with operator new: I have no idea what you are talking about. I am totally gone, left in the dust. What is this stuff? Why do you need new to point to memory locations, I thought that is what char* did?

Steve: You're right that char* points to a memory location. But which one? The purpose of new is to get some memory for us from the operating system and return the address of the first byte of that memory. In this case, we assign that address to our char* variable called m_Data. Afterward, we can store data at that address.

Susan: I am not getting this because I just don't get the purpose of char*, and don't just tell me that it points to an address in memory. I want to know why we need it to point to a specific address in memory rather than let it take on just any random address in memory.

Steve: Because then there would be no way of guaranteeing that the memory that it points to won't be used by some other part of the program or indeed some other program entirely in a multitasking system like Windows. We need to claim ownership of some memory by calling new before we can use it.

Susan: I think I understand now why we need to use new, but why should the result of calling new be a pointer? I am missing this completely. How does new result in char*? Some steps are not obvious here.

Steve: Because that's how new is defined: it gives you an address (pointer) to a place where you can store some chars (or whatever type you requested).

Susan: OK, but in the statement m_Data = new char [m_Length], why is char in this statement not char*? I am getting so confused on this.

Steve: Because you're asking for an address (pointer) to a place where you can store a bunch of chars.

Susan: But then wouldn't it be necessary to specify char* rather than char in the statement?

Steve: I admit that I find that syntax unclear as well. Yes, in my opinion, it should be stated as char*, but apparently Bjarne thought otherwise.

Susan: OK, so then m_Data is the pointer address where new (memory from the heap) is going to store char data of m_Length. Right?

Steve: Yes, the value assigned to m_Data in the constructor is the value returned from new; this value is the address of an area of memory allocated to this use. The area of memory is of length m_Length.

Susan: Well, I thought that the address stored in m_Data was the first place where you stored your chars. So is new just what goes and gets that memory to put the address in m_Data?

Steve: Exactly.

Susan: Here's what I understand about the purpose of char*. It functions as a pointer to a specific memory address. We need to do that because the computer is stupid and doesn't know where to put the char data, therefore we need char* to say "hey you, computer, look over here, this is where we are going to put the data for you to find and use".

Steve: That's fine.

Susan: We need to use char* for variable length memory. This is because we don't know how much memory we will need until it is used. For this we need the variable m_Data to hold the first address in memory for our char data. Then we need the variable m_Length that we have set to the length of the C string that will be used to get the initial data for the string. Then we have to have that nifty little helper guy new to get some memory from the heap for the memory of our C string data.

Steve: Sounds good to me.

Susan: Now about memcpy: This appears to be the same thing as initializing the variable. I am so confused.

Steve: That's exactly correct. Maybe you shouldn't get unconfused!

As the call to memcpy is the only statement in the constructor proper, it's time to see what we have accomplished. The constructor has initialized a string by

  1. Setting the length of the string to the effective length of a null C string, "", including the terminating null byte (i.e., 1 byte).
  2. Allocating memory for a null C string.
  3. Copying the contents of a null C string to the allocated memory.

Now let's continue with our examination of the string::string() constructor. Its final result is a string with the value "", whose memory layout might look like Figure strempty.5

An empty string in memory (Figure strempty)

Address   Name             string n                
                     +--------------------+                 
12340000  m_Length   |        0001        |                
                     +--------------------+                 
12340002  m_Data     |      1234febc      +-+                 
                     +--------------------+ |                
                                            |                
                     +--------------------+ |                
1234febc  (none)     |         00         |±+                
                     +--------------------+  

Using the default constructor is considerably easier than defining it. As we have seen in Chapter inventor.htm, the default constructor is called whenever we declare an object without specifying any data to initialize it with; for example, in the line string s; in Figure strtst1. Although this program doesn't do anything useful, it does illustrate how we can use the member functions of the string class, so you should pay attention to it.

Our first test program for the string class (code\strtst1.cc) (Figure strtst1)

code/strtst1.cc

In case it's still not clear why the line string s; calls the default constructor string::string(), here's the detailed explanation:

  1. The line string s; means that we want to create a variable of type string with the name s.
  2. Therefore, since string is not a native data type, the compiler looks for a function called string::string, which would create a string. It finds one because we've included the file string1.h in our example program; the contents of that file are listed in Figure stringinterface1.
  3. However, we can have several functions named string::string, with different argument lists, because there are several possible ways to supply the initial data for a string we're creating. In this case, we aren't supplying any initial data, so the default constructor string::string() is the only possibility.
  4. Since that function string::string() is declared in the header file, the line string s; is translated to a call to that function.

I should point out here that the only file that the compiler needs to figure out how to compile the line string s; is the header file, string1.h. The actual implementation of the string class in string1.cc isn't required, because all the compiler cares about when compiling a program using classes is the contract between the class implementer and the user; that is, the header file. The actual implementation in string1.cc that fulfills this contract isn't needed until the program is linked to make an executable; at that point, the linker will complain if it can't find an implementation of any function that we've referred to.

Caution: Construction Area

Now that we've disposed of the default constructor, let's take a look at the line in the string interface definition (Figure stringinterface1) that says string(char* p);.6 This is the declaration for another constructor; unlike the default constructor we've examined, this one has an argument, char* p.7

As we saw in Chapter inventor.htm, the combination of the function name and argument types is called the signature of a function. Two functions that have the same name but differ in the type of at least one argument are distinct functions, and the compiler will use the difference(s) in the type(s) of the argument(s) to figure out which function with a given name should be called in any particular case. Of course, this leads to the question of why we would need more than one string constructor; they all make strings, don't they?

Yes, they do, but not from the same "raw material". It's true that every constructor in the string class makes a string, but each constructor has a unique argument list, which determines exactly how the new string will be constructed. The default constructor always makes an empty string (like the C string ""), whereas the constructor string(char* p) takes a C string as an argument and makes a string that has the same value as that argument.

Susan wasn't going to accept this without a struggle.

Susan: I don't get "whereas the string(char* p) constructor takes a C string and makes a string that has the same value as the C string does."

Steve: Well, when the compiler looks at the statement string n("Test"); it has to follow some steps to figure it out.

  1. The compiler knows that you want to create a string because you've defined a variable called n with the type string; that's what string n means.
  2. Therefore, since string is not a native data type, the compiler looks for a function called string::string, which would create a string.
  3. However, there can be several functions named string::string, with different argument lists, because there are several possible ways to get the initial data for the string you're creating. In this case, you are supplying data in the form of a literal C string, whose type is char*; therefore, a constructor with the signature string::string(char*) will match.
  4. Since a function with the signature string::string(char*) has been declared in the header file, the line string n("Test"); is translated to a call to that function.

Susan: So string(char* p) is just there in case you need it for "any given situation"; what situation is this?

Steve: It depends on what kind of data (if any) we're supplying to the constructor. If we supply no data, then the default constructor is used. If we supply a C string (such as a literal C string), then the constructor that takes a char* is used, because the type of a C string is char*.

Susan: So string s; is the default constructor in case you need something that uses uninitialized objects?

Steve: Not quite; the default constructor for the string class is string::string(), which doesn't need any arguments, because it constructs an empty string.

Susan: And the string n ("Test"); is a constructor that finally gets around to telling us what we are trying to accomplish here?

Steve: Again, not quite. The line string n("Test"); causes a string with the value "Test" to be created, by calling the constructor string::string(char* p);.

Susan: See, you are talking first about string n("Test"); in Figure strtst1 and then you get all excited that you just happen to have string::string(char* p) hanging around which is way over in Figure stringinterface1.

Steve: Now you know that a literal C string such as "Test" has the data type char*. Does this make sense?

Susan: OK, I think this helped. I understand it better. Only now that I do, it raises other questions that I accepted before but now don't make sense due to what I do understand. Does that make sense to you? I didn't think so.

Steve: Sure, why not? You've reached a higher level of understanding, so you can now see confusions that were obscured before.

Susan: So this is just the constructor part? What about the default constructor, what happened to it?

Steve: We can't use it here, because we have some data to assign to the string when the string is created. A default constructor is used only when there is no initial value for a variable.

Susan: So was the whole point of discussion about default constructors just to let us know that they exist even though you aren't really using them here?

Steve: We are using them to create strings with no initial value, as discussed before.

Susan wasn't clear on why the C string "Test" would be of type char*, which is understandable because that's anything but obvious. Here's the discussion we entered into on this point.

Susan: When you say "Test" is a literal C string of type char* and that the compiler happily finds that declaration, that is fine. But see, it is not obvious to me that it is type char*; I can see char but not char*. Something is missing here so that I would be able to follow the jump from char to char*.

Steve: A literal C string isn't a single char, but a bunch of chars. Therefore, we need to get the address of the first one; that gives us the addresses of the ones after it.

Now that the reason why a literal C string is of type char* is a bit clearer, Figure stringcharstarcons shows the implementation for the constructor that takes a char* argument.

The char* constructor for the string class (from code\string1.cc) (Figure stringcharstarcons)

string::string(char* p)

: m_Length(strlen(p) + 1),

m_Data(new char [m_Length])

{

memcpy(m_Data,p,m_Length);

}

You should be able to decode the header string::string(char* p): This function is a constructor for class string (because its class is string and its name is also string); its argument, named p, is of type char*. The first member initialization expression is m_Length(strlen(p) + 1). This is obviously initializing the string's length (m_Length) to something, but what?

As you may recall, C strings are stored as a series of characters terminated by a null byte (i.e., one with a 0 value). Therefore, unlike the case with our strings, where the length is available by looking at a member variable (m_Length), the only way to find the length of a C string is to search from the beginning of the C string until you get to a null byte. Since this is such a common operation in C, the C standard library provides the function strlen (short for "string length") for this purpose; it returns a result indicating the number of characters in the C string, not including the null byte. So the statement m_Length = strlen(p) + 1; sets our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (not including the terminating null byte) + 1 for the terminating null byte.8 So the member initialization expression m_Length(strlen(p) + 1), initializes our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (which doesn't include the terminating null byte) + 1 for the terminating null byte. We need this information because we've decided to store the length explicitly in our string class rather than relying solely on a null byte to mark the end of the string, as is done in C.9 Susan had some questions about the implementation of this function, and I supplied some answers.

Susan: What is strlen?

Steve: A function left over from C; it tells us how long a C string is.

Susan: Where did it come from?

Steve: It's from the standard C library, which is part of C++.

Susan: What are you using it for here?

Steve: Finding out how long the C string is that we're supposed to copy into our string.

Susan: Is this C or C++?

Steve: Both.

Susan: Why is char* so special that it deserves a pointer? What makes it different?

Steve: The * means "pointer". In C++, char* means "pointer to a char".

Susan: I just don't understand the need for the pointer in char. See when we were using it (char) before, it didn't have a pointer, so why now? Well, I guess it was because I thought it was native back then when I didn't know that there was any other way. So why don't you have a pointer to strings then? Are all variables in classes going to have to be pointed to? I guess that is what I am asking.

Steve: We need pointers whenever we want to allocate an amount of memory that isn't known until the program is executing. If we wanted to have a rule that all strings could be only 10 characters in length (for example), then we could allocate the space for those characters in the string. However, we want to be able to handle strings of any length, so we have to decide how much space to allocate for the data when the constructor string::string(char* p) is called; the only way to do that is to use a pointer to memory that is allocated at run time, namely m_Data. Then we can use that memory to hold a copy of the C string pointed to by the parameter p.

Susan: Oh, no! Here we go again. Is m_Data a pointer? I thought it was just a variable that held an address.

Steve: Those are equivalent statements.

Susan: Why does it point? (Do you know how much I am beginning to hate that word?) I think you are going to have to clarify this.

Steve: It "points" in a metaphorical sense, but one that is second nature to programmers in languages like C. In fact, it merely holds the address of some memory location. Is that clearer?

Susan: So the purpose of m_Data is just a starting off point in memory?

Steve: Right. It's the address of the first char used to store the value of the string.

Susan: So the purpose of m_Length is to allot the length of memory that starts at the location where m_Data is?

Steve: Close; actually, it's to keep track of the amount of memory that has been allocated for the data.

Susan: But I see here that you are setting m_Length to strlen, so that in effect makes m_Length do the same thing?

Steve: Right; m_Length is the length of the string because it is set to the result returned by strlen (after adding 1 for the null byte at the end of the C string).

Susan: Why would you want a string with no data, anyway? What purpose does that serve?

Steve: So you can define a string before knowing what value it will eventually have. For example, the statement string s; defines a string with no data; the value can be assigned to the string later.

Susan: Oh yeah, just as you would have short x;. I forgot.

Steve: Yep.

Susan: Anyway, the first thing that helped me understand the need for pointers is variable-length data. I am sure that you mentioned this somewhere, but I certainly missed it. So this is a highlight. Once the need for it is understood then the rest falls in place. Well, almost; it is still hard to visualize, but I can.

Steve: I'll make sure to stress that point until it screams for mercy.

Susan: I think you might be able to take this information and draw a schematic for it. That would help. And show the code next to each of the steps involved.

Steve: Don't worry, we'll see lots of diagrams later.

Susan: So strlen is a function like any member function?

Steve: Yes, except that it is a global function rather than a member function belonging to a particular class. That's because it's a leftover from C, which doesn't have classes.

Susan: So it is what I can consider as a native function? Now I am getting confused again. I thought that just variables can be either made up (classes) or native. Why are we talking about functions in the same way? But then I remember that, in a backward way, functions belong to variables in classes rather than the other way around. This is just so crazy.

Steve: Functions are never native in the way that variables are; that is, built into the language. A lot of functions come with the language, in the form of the libraries, but they have no special characteristics that make them "better" than ones you write yourself. However, this is not true of variables; in C, it's impossible to add variable types that have the appearance and behavior of the native types.

Susan: You see I think it is hard for me to imagine a function as one word, because I am so used to main() with a bunch of code following it and I think of that as the whole function; see where I am getting confused?

Steve: When we call a function like strlen, that's not the whole function, it's just the name of the function. This is exactly like the situation where we wrote Average and then called it later to average some numbers.

Susan: A function has to "do something", so you will have to define what the function does; then when we use the function, we just call the name and that sets the action in gear?

Steve: Exactly.

Susan: Now, about this char* thing. . . (don't go ballistic, please) . . . exactly what is it? I mean it is a pointer to char, so what is *? Is it like an assignment operator? How is it classified?

Steve: * means "pointer to the type preceding". So char* means "pointer to char", short* means "pointer to short", and so on.

Susan: So that would be for a short with variable-length data? And that would be a different kind of short than a native short?

Steve: Almost, but not quite, correct. It would be for variable-length data consisting of one or more shorts, just as a literal C string is variable-length data consisting of one or more chars.

Susan: So it would be variable by virtue of the number of shorts?

Steve: Actually, by virtue of the possibility of having a number of shorts other than exactly one. If you might need two or three (or 100, for that matter) shorts (or any other type), and you don't know how many when the program is compiled, then you pretty much have to use a pointer.

Susan: OK, yes, you said that about * and what it means to use a char*, but I thought it would work only with char so I didn't know I would be seeing it again with other variable types. I can't wait.

Steve: Luckily, it appears that we will avoid pointers to types other than char, due to space constraints. I'll bet you're terribly disappointed.

The next member initialization expression in the constructor, m_Data(new char [m_Length]), is the same as the corresponding expression in the default constructor; in this case, of course, the amount of memory being allocated is equal to the length of the input C string (including its terminating null byte), rather than the fixed value 1.

Now that we have the address of some memory that belongs to us, we can use it to hold the characters that make up the value of our string. The literal value that our test program uses to call this constructor is "Test", which is four characters long, not counting the null byte at the end; since we have to make room for that null byte, the total is 5 bytes, so that's what we'd ask for from new. Assuming that the return value from new was 1234febc, Figure strcons3 illustrates what our new string looks like at this point.

string n during construction (Figure strcons3)

Address   Name             string n                
                     +--------------------+                 
12340000  m_Length   |        0005        |                
                     +--------------------+                 
12340002  m_Data     |      1234febc      +-+                 
                     +--------------------+ |                
                                            |                
                     +--------------------+ |                
1234febc  (none)     |        ????        |±+                
                     +--------------------+  

The reason for the ???? is that we haven't set the data at that location to any value yet, so we don't know what it contains. Actually, this brings up a point we've skipped so far, where new gets the memory it allocates. The answer is that all of the "free" memory in your machine (i.e., memory that isn't used to store the operating system, the code for your program, statically allocated variables, and the stack) is lumped into a large area called the heap. This is where dynamically allocated memory "lives".10 That's a loose way of stating what actually happens, which is that new cordons off part of the heap as being "in use" and returns a pointer to that portion.

It's possible that the idea of a variable that holds a memory address but which is itself stored in memory isn't that obvious. It wasn't to Susan:

Susan: I don't get this stuff about a pointer being stored in a memory address and having a memory address in it. So what's the deal?

Steve: Here's an analogy that might help. Have you ever seen a set of mailboxes for an apartment building? The regular mailboxes are pretty small, but what do they do when you get something that is too big to fit into your regular mailbox? One solution is to put the larger object into one of a few large mailboxes, and leave the key to the larger mailbox in your regular mailbox.

So at this point, we have allocated m_Length bytes of memory, which start at the address in the pointer variable m_Data. Now we need to copy the current value of the input C string (pointed to by p) into that newly allocated area of memory. This is the job of the sole statement inside the constructor proper {}, memcpy(m_Data,p,m_Length); which copies the data from the C string pointed to by p to our newly allocated memory.

The final result is that we have made (constructed) a string variable and set it to a value specified by a C string. To see what our string might look like in memory, see Figure strnmem. But how would this string::string(char* p) constructor operate in a program? To answer that question, Figure strtst1x gives us another look at our sample program.

A simple test program for the string class (code\strtst1.cc) (Figure strtst1x)

code/strtst1.cc

Constructive Criticism?

How does the compiler interpret the line string n("Test");? First, it determines that string is the name of a class. A function with the name of a class, as we have already seen, is always a constructor for that class. The question is which constructor to call; the answer is determined by the type(s) of the argument(s). In this case, the argument is a literal C string, which has the type char*; therefore, the compiler looks for a constructor for class string that has an argument of type char*. Since there is such a constructor, the one we have just examined, the compiler generates a call to it. Figure strcharstarx shows it again for reference while we analyze it.

The char* constructor for the string class, again (from code\string1.cc) (Figure strcharstarx)

string::string(char* p)

: m_Length(strlen(p) + 1),

m_Data(new char [m_Length])

{

memcpy(m_Data,p,m_Length);

}

When the program executes, string::string(char* p) is called with the argument "Test". Let's trace the execution of the constructor, remembering that member initialization expressions are executed in the order in which the member variables being initialized are listed in the class interface, not necessarily in the order in which they are written in the member initialization list.

  1. The first member initialization expression is m_Length(strlen(p) + 1). This initializes the member variable m_Length to the length of the C string whose address is in p, including the null byte that terminates the string. In this case, the C string is "Test", and its length, including the null byte, is 5.
  2. Next, the member initialization expression m_Data(new char [m_Length]) is executed. This allocates m_Length (5, in this case) bytes of memory from the heap and initializes the variable m_Data to the address of that memory.
  3. Finally, the statement memcpy(m_Data,p,m_Length); copies m_Length bytes (5, in this case) of data from the C string pointed to by p to the memory pointed to by m_Data.

When the constructor is finished, the string variable n has a length, 5, and contents, "Test", as shown in Figure strnmem. It's now ready for use in the rest of the program. After all the discussion, Susan provided this rendition of the char* constructor for the string class.

Susan: So first we define a class. This means that we will have to have one or more constructors, which is a function with the same name as the class, used to create objects of that class. The char* constructor we're dealing with here goes through three steps, as follows: Step 1 sets the length of the string; step 2 gets the memory to store the data, and provides the address of that memory; step 3 does the work; it copies what you want.

Steve: Right.

Tricky Assignment

Now, let's look at the next line: s = n;. That looks harmless enough; it just copies one string, n, to another string, s.11 But wait a second; how does the compiler know how to assign a value to a variable of a type we've made up? Just as the compiler will generate a version of the default constructor if we don't define one, because every object has to be initialized somehow, the ability to assign one value of a given type to a variable of the same type is essential to being a data type; therefore, the compiler will supply a version of operator =, the assignment operator, if we don't define one ourselves. In Chapter inventor.htm, we were able to rely on the compiler-generated operator =, which simply copies every member variable from the source object to the target object. That was perfectly fine for our StockItem and Inventory objects, so wouldn't it do the job here? Unfortunately, no. The reason is that the member variable m_Data isn't really the data for the string; it's a pointer to (i.e., the address of) the data. The compiler-generated =, however, doesn't know how we're using m_Data, so it copies the pointer rather than the data. In our example, s = n;, the member variable m_Data in s would end up pointing to the same place in memory as the member variable m_Data in n. Thus, if either s or n did something to change "its" data, both strings would have their values changed, which isn't how we expect variables to behave.

To make this more concrete, let's suppose that our string object n looks like Figure strnmem in memory.

string n in memory (Figure strnmem)

Address   Name             string n                
                     +--------------------+                 
12340000  m_Length   |        0005        |                
                     +--------------------+                 
12340002  m_Data     |      1234febc      +-+                 
                     +--------------------+ |                
                                            |                
                     +--------+             |                 
1234febc  (none)     |   "T"  |±------------+                 
                     +--------+                  
                     +--------+                
1234febd  (none)     |   "e"  |                
                     +--------+                  
                     +--------+                
1234febe  (none)     |   "s"  |                
                     +--------+                
                     +--------+                
1234febf  (none)     |   "t"  |                
                     +--------+                
                     +--------+                
1234fec0  (none)     |    0   |                
                     +--------+

So far, so good. We have an object of type string, which contains a length and a pointer to a dynamically allocated memory area where its actual data are stored. However, if we use the compiler-generated operator = to execute the statement s = n;, the result looks like Figure strnsmem1.

strings n and s in memory after compiler-generated = (Figure strnsmem1)

 

Address   Name             string n                
                     +--------------------+                 
12340000  m_Length   |        0005        |                 
                     +--------------------+                 
12340002  m_Data     |      1234febc      +-+                 
                     +--------------------+ |                
                                            |                
                                            |                            
                     +--------+             |                 
1234febc  (none)     |   "T"  |±------------+                 
                     +--------+             |                
                     +--------+             |                
1234febd  (none)     |   "e"  |             |                
                     +--------+             |                
                     +--------+             |                
1234febe  (none)     |   "s"  |             |                
                     +--------+             |                
                     +--------+             |                
1234febf  (none)     |   "t"  |             |                
                     +--------+             |                
                     +--------+             |                
1234fec0  (none)     |    0   |             |                
                     +--------+             |                
                                            |                
                           string s         |                
                     +--------------------+ |                
12340020  m_Length   |        0005        | |                
                     +--------------------+ |                
12340022  m_Data     |      1234febc      +-+                 
                     +--------------------+  

In other words, the two strings s and n are like Siamese twins; whatever affects one of them affects the other, since they share one copy of the data "Test". What we really want is two strings that are independent of one another, so that we can change the contents of one without affecting the other one. Very shortly, we'll see how to accomplish this.

As you might suspect, Susan didn't think the need for us to define our own operator = was obvious at all. Here's how I started talking her into it.

Susan: I have a little note to you off to the side in the margins about this operator =, it says "If it was good enough for native data then why not class data?" I think that is a very good question, and I don't care about that pointy thing. I don't understand why m_Data isn't really data for the string; it is part of the string, so why isn't it part of its data?

Steve: It points to the data for the string, which is a bunch of characters. It isn't the characters itself, but their address.

Susan: Actually, looking at these figures makes this whole idea more understandable. Yes, I see somewhat your meaning in Figure strnsmem1; that pointy thing is pointing all over the place. Oh no, I don't want to see how to make two independent strings! Just eliminate the pointy thing and it will be all better. OK?

Steve: Sorry, that isn't possible. You'll just have to bear with me until I can explain it to you better.

Susan: Well, let me ask you this: Is the whole point of writing the statement s=n just to sneak your way into this conversation about this use of operator =? Otherwise, I don't see where it would make sense for the sample program.

Steve: Yes, that's correct.

Susan: And the chief reason for creating a new = is that the new one makes a copy of the data using a new memory address off the heap, rather than having the pointer pointing to the same address while using the compiler-generated operator =? If so, why? Getting a little fuzzy around that point. With StockItem, the compiler-generated operator = was good enough. Why not now?

Steve: Yes, that's why we need to create our own operator =. As for why we didn't need one before: Since the components of a StockItem are all concrete data types, we don't have to worry about "sharing" the data as we do with the string class, which contains a char*.

Susan: So when you use char* or anything with a pointer, that is outside the realm of concrete data types?

Steve: Right. However, the reason that we can't allow pointers to be copied as with the compiler-generated operator = isn't that they aren't concrete data types, but that they aren't the actual data of the strings. They're the address of the actual data; therefore, if we copy the pointer in the process of copying a variable, both pointers hold the same address. This means that changes to one of the variables affects the other one, which is not how concrete data types behave.

Susan: I think I actually understand this now. At least, I'm not as confused as I was before.

Steve: Good; it's working.

Assignment of Responsibility

Although it's actually possible to get the effect of two independent strings without the extra work of allocating memory and copying data every time an assignment is done, the mechanisms needed to do that are beyond the scope of this book. By far the easiest way to have the effect of two independent strings is to actually make another copy of a string's data whenever we copy the string, and that's how we'll do it here. The results will be as indicated in Figure strnsmem2.

strings n and s in memory after custom = (Figure strnsmem2)

Address   Name             string n                
                     +--------------------+                 
12340000  m_Length   |        0005        |                 
                     +--------------------+                 
12340002  m_Data     |      1234febc      +-+                 
                     +--------------------+ |                
                     +--------+             |                 
1234febc  (none)     |   "T"  |±------------+                 
                     +--------+                  
                     +--------+                
1234febd  (none)     |   "e"  |                
                     +--------+                  
                     +--------+                
1234febe  (none)     |   "s"  |                
                     +--------+                
                     +--------+                
1234febf  (none)     |   "t"  |                
                     +--------+                
                     +--------+                
1234fec0  (none)     |    0   |                
                     +--------+                
                                                                         
                           string s                                      
                     +--------------------+                              
12340020  m_Length   |        0005        |                              
                     +--------------------+                             
12340022  m_Data     |      12345600      +-+                 
                     +--------------------+ |                
                                            |                
                     +--------+             |                 
12345600  (none)     |   "T"  |±------------+                 
                     +--------+                  
                     +--------+                
12345601  (none)     |   "e"  |                
                     +--------+                  
                     +--------+                
12345602  (none)     |   "s"  |                
                     +--------+                
                     +--------+                
12345603  (none)     |   "t"  |                
                     +--------+                
                     +--------+                
12345604  (none)     |    0   |                
                     +--------+

With this arrangement, a change to one of the string variables will leave the other one unaffected, as the user would expect. To make this happen, we have to implement our own operator =, which will copy the data rather than just the pointer to the data. That's the operator declared in Figure stringinterface1 by the following line:

string& operator = (const string& Str);

What exactly does this mean? Well, as with all function declarations, the first part of the function declaration indicates the return type of the function. In this case, we're going to return a reference to the string to which we're assigning a value; that is, the string on the left of the = sign in an assignment statement. While this may seem reasonable at first glance, actually it's not at all obvious why we should return anything from operator =. After all, if we say a = b;, after a has been set to the same value as b, we're done; that operation is performed by the = operator, so no return value is needed after the assignment is completed.

However, there are two reasons why assignment of native types returns a value, which is equal to the value that was assigned to the left hand argument of =. First, it allows us to write an if statement such as if (a = b), when we really meant if (a == b); of course, this will cause a bug in the program, since these two statements don't have the same meaning. The first one sets a to b and returns the value of a; if a isn't 0, then the if condition is considered true. The latter statement, of course, compares a and b for equality and makes the if condition true if they are equal. To help prevent the error of substituting = for == in this situation, many compilers have a warning that indicates your use of, say, if (a = b); unfortunately, this is a legal construction with native types, and so cannot generate a compiler error. As it happens, using = in this way is an illegal operation with class objects, so even if you want to use this error-prone construction, you can't. Since I never use that construction with native variables, I don't mind not having it for class objects.

The other potential use of the return value from operator = is to allow statements such as a = b = c; where the current value of c is assigned to b and the return value from that assignment is assigned to a. Although I don't use that construction either, since I find it more confusing than useful, I have been told that this ability will be required to use some of the library facilities specified in the forthcoming C++ standard. Therefore, it is my obligation to teach you the "right" way to write assignment operators so that you will be able to use these standard facilities with your classes.

Now we're up to the mysterious looking construct operator =. This portion of the function declaration tells the compiler the name of the function we're defining; namely, operator =. The operator keyword lets the compiler know that the "name" of this function is actually an operator name, rather than a "normal" function name. We have to say operator = rather than merely =, for two reasons. First, because normal function names can't have a = character in them, but are limited to upper and lower case letters, numbers, and the underscore (_). Second, because when we're redefining any operator, even one (like new) whose name is made of characters allowed in identifiers, we have to tell the compiler that we're doing that on purpose. Otherwise, we'll get an error telling us that we're trying to define a function or variable with the same name as a keyword.12 We're ready to look at the argument to this function, specified by the text inside the parentheses, const string& Str. We've already seen in Chapter inventor.htm that & in this context means that the argument to which it refers is a reference argument rather than a value argument.13 In other words, the variable Str is actually just another name for the argument provided by the caller of this function, rather than being a separate local variable with the same value as the caller's argument. However, there is a new keyword in this expression: const, which is short for "constant". In this context, it means that we promise that this function will not modify the argument to which const refers, namely string& Str. This is essential in the current situation, but it will take some discussion to explain why.

References Required

As you may recall from Chapter function.htm, when you call a function using a value argument, the argument that you give in the calling function isn't the one that the called function receives. Instead, a copy is made of the calling function's argument, and the called function works on the copy. While this is fine most of the time, in this case it won't work properly, for reasons that will be apparent shortly; instead, we have to use a reference argument. As we saw in the discussion of reference arguments in Chapter inventor.htm, such an argument is not a copy of the caller's argument, but another name for the actual argument provided by the caller. This has a number of consequences. First, it's more efficient than a "normal" argument, because the usual processing time needed to make a copy for the called function isn't required. Second, any changes made to the reference argument change the caller's argument as well. The use of this mechanism should be limited to those cases where it is really necessary, since it can confuse the readers of the calling function. There's no way to tell just by looking at the calling function that some of its variables can be changed by calling another function.

In this case, however, we have no intention of changing the input argument. All we want to do is to copy its length and data into the output string, the one for which operator = was called. Therefore, we tell the compiler, by using the const modifier, that we aren't going to change the input argument. This removes the drawback of non-const reference arguments: that they can change variables in the calling function with no indication of that possibility in the calling function. Therefore, using const reference arguments is quite a useful and safe way to reduce the number of time-consuming copying operations needed to make function calls.

The use of a const reference argument in this case is more than just efficient, however; as we'll see in the discussion in Chapter downpath.htm under the heading "Temporary Help Wanted", such an argument allows us to assign a C string (i.e., bytes pointed to by a char*) to one of our string variables without having to write a special operator = for that purpose.

You might be surprised to hear that Susan didn't have too much trouble accepting all this stuff about const reference arguments. Obviously her resistance to new ideas was weakening by this point.

Susan: OK, so the reference operator just renames the argument and doesn't make a copy of it; that is why it is important to promise not to change it?

Steve: Right. A non-const reference argument can be changed in the function, because unlike a "regular" (i.e., value) argument, which is really a copy of the calling function's variable, a reference argument is just another name for the caller's variable. Therefore, if we change the reference argument, we're really changing the caller's variable, which is generally not a good idea.

Susan: OK. But in this case since we are going to want to change the meaning of = in all strings it is OK?

Steve: Not quite. Every time we define an operator we're changing the meaning of that operator for all objects of that class. The question is whether we're intending to change the value of the caller's variable that is referred to by the reference argument. If we are, then we can't use const to qualify the reference; if not, we can use const. Does that answer your question?

Susan: Well, yes and no. I think I have it now: When you write that code it is for that class only and won't affect other classes that you may have written, because it is contained within that particular class code. Right?

Steve: Correct.

Susan: So we don't want to change the input argument because what we are doing is basically defining a new = for this class, right?

Steve: Right. The input argument is where we get the data to copy to the string we're assigning to. We don't want to change the input argument, just the string we're assigning to.

Back to the discussion of the function declaration, we now have enough information to decode the function declaration

string& string::operator = (const string& Str)

as illustrated in Figure operatoreqstring.

The declaration of operator = for the string class (Figure operatoreqstring)

This function returns a reference to a string;                
                
  |      it belongs to the string class;                  
  |          |   it implements operator =;                
  |          |                
  |          |         |   and it won't change its argument,                
  |          |         |              which is a reference to                 
  |          |         |           |  the caller's string;                 
  |          |         |           |  i.e., another name for                
  |          |         |           |  the caller's variable.                
  |          |         |           |      |                
  |          |         |           |      |                                             
+-+-----++---+----++---+------+ +--+--++--+--------+                 
 string&  string::  operator = ( const  string& Str )

Putting it all together, we're defining a function belonging to class string that returns a reference to a string. This function implements operator = and takes an argument named Str that's a constant reference to a string. That is, the argument Str is another name for the string passed to us by the caller, not a copy of the caller's string. Furthermore, we're vowing not to use this argument to change the caller's variable.

Hello, operator?

Now that we've dissected the header into its atomic components, the actual implementation of the function should be trivial by comparison. But first there's a loose end to be tied up. That is, why was this function named string::operator = called in the first place? The line that caused the call was very simple: s = n;. There's no explicit mention of string or operator.

This is another of the ways in which C++ supports classes. Because you can use the = operator to assign one variable of a native type to another variable of the same type, C++ provides the same syntax for user defined variable types. Similar reasoning applies to operators like >, <, and so on, for classes where these operators make sense.

When the compiler sees the statement s = n;, it proceeds as follows:

  1. The variable s is an object of class string.
  2. The statement appears to be an assignment statement (i.e., an invocation of the C++ operator named operator =) setting s equal to the value of another string value named n.
  3. Is there a definition of a member function of class string that implements operator = and takes one argument of class string?
  4. Yes, there is. Therefore, translate the statement s = n; into a call to operator = for class string.
  5. Compile that statement as though it were the one in the program.

Susan was appreciative of the reminder that we started out discussing the statement s = n;.

Susan: Oh, my gosh, I totally forgot about s = n; thanks for the reminder. We did digress a bit, didn't we? Are you saying you have to go through the same thing to define other operators in classes?

Steve: Yes.

Susan: So are you saying that when you write the simple statement s = n; that the = calls the function that we just went through?

Steve: Right.

Following this procedure, the correspondence between the tokens in the original program and the call to the member function should be fairly obvious, as we see them in Figure callingopeq.14

Calling the operator = implementation (Figure callingopeq)

        s = n;                
                        
          | |                 
          | +-----------------------+                 
          +----------+              |                
                +----+-----+ +------+----------+                 
string& string:: operator = ( const string& Str )

But we've left out something. What does the string s correspond to in the function call to operator =?

What Is the Meaning of this?

The string s corresponds to a hidden argument whose name is the keyword this. Such an argument is automatically included in every call to a member function in C++.15 Its type is always a constant pointer to an object of the class that a member function belongs to. In this case its type is const string*; that is, a constant pointer to a string; the const means that we can't change the value of this by assigning a new value to it. The value of this is the address of the class object for which the member function call was made. In this case, the statement s = n; was translated into s.operator = (n); by the compiler; therefore, when the statement s = n; is being executed, the value of this is the address of the string s.

To see why we need to be concerned about this, let's start analyzing the implementation of operator =. Figure stringassignment shows the code for that function.

The assignment operator (operator =) for the string class (from code\string1.cc) (Figure stringassignment)

string& string::operator = (const string& Str)

{

if (&Str != this)

{

delete [] m_Data;

m_Length = Str.m_Length;

m_Data = new char [m_Length];

memcpy(m_Data,Str.m_Data,m_Length);

}

return *this;

}

Equality Now!

In the case of the string class, as in most cases, the only reasons why we have to worry about this are to be able to determine whether two objects that are referred to by different names are actually the same object and to return a reference to the left-hand object in an assignment statement. You can see both uses of this in the code for string::operator = (const string& Str). The first use is illustrated by the following line:

if (&Str != this)

Most of this statement should be familiar by now. Like other if statements, it tests a condition for truth or falsity. The != is the comparison operator that tests for "not equal", and of course we've just seen that this is the address of the variable for which the operator = function was called; in the expression s = n, this would be the address of the variable s. However, that still leaves the expression &Str.

Unfortunately, & is one of the tokens that is used in several different ways depending on context; when it precedes the name of a variable without itself being preceded by a data type like string, it means "the address of the following variable". In this case, &Str means "the address of Str". The variable Str is the argument passed to us by the caller; remember, as a reference argument, Str is another name for the caller's variable, rather than a copy of it. Therefore, &Str is the address of the caller's variable (n, in our example).

Clearly, then, the expression if (&Str != this) is comparing the address of the caller's string (i.e., &Str) to this. As we just saw, this represents the address of the object that we're operating on; in the case of operator =, it's the address of (i.e., a pointer to) the string that is going to be changed by operator =. Therefore, this if statement is checking whether the string that was passed to us by the caller is a different string from the one to which the assignment is supposed to be made. For example, if the source line that caused operator = to be called was a = a; then the if statement would be false and therefore the block controlled by the if wouldn't be executed. Of course, there's no reason to do anything other than return the value of a in that case. However, there are good reasons besides efficiency to check for the attempt to assign a string to itself; those reasons are the subject of one of the exercises in this chapter. For now, let's continue with the contents of the block controlled by the if, assuming that the two strings are actually distinct.

The first statement in the controlled block of the if statement is delete [] m_Data;. This corresponds to the new statement that we used to allocate memory for a string in the constructor string::string(char* p) (Figure stringcharstarcons).16 That is, the delete operator returns the memory to the available pool called the heap. There are actually two versions of the delete operator: One version frees memory for a single data item, and the other frees memory for a group of items that are stored consecutively in memory. Here, we're using the version of the delete operator that frees a group of items rather than a single item, which we indicate by means of the [] after the keyword delete; the version of delete that frees only one item doesn't have the [].17 So after this statement is executed, the memory that was allocated in the constructor to hold the characters in our string has been handed back to the memory allocation routines for possible reuse at a later time.

Susan had a few minor questions about this topic, but nothing too alarming.

Susan: So delete just takes out the memory new allocated for m_Data?

Steve: Right.

Susan: What do you mean by "frees a group of items"?

Steve: It returns the memory to the heap, so it can be used for some other purpose.

Susan: Is that all the addresses in memory that contain the length of the string?

Steve: Not the length of the string, but the data for the string, such as "Test".

Please delete Me, Let Me Go

A point that we should not overlook is the possibility of calling delete for a pointer that has never been assigned a value. Calling delete on a pointer that doesn't point to a valid block of memory allocated by new will cause the system to malfunction in some bizarre way, usually at a time considerably after the improper call to delete.18 This occurs because the dynamic memory allocation system will try to reclaim the "allocated" memory pointed to by the invalid pointer by adding it back to the heap. Eventually, some other function will come along, ask for some memory, and be handed a pointer to this "available" block that is actually nothing of the sort. The result of trying to use this area of memory depends on which of three cases the erroneous address falls into: The first is that the memory at that address is nonexistent, the second is that the memory is already in use for some other purpose, and the third is that the invalid address points to data that is already in the heap. In the first case, the function that tries to store its data in this nonexistent area of memory will cause a system crash or error message, depending on the system's ability and willingness to check for such errors; the "General Protection Fault" message so familiar to Windows users is caused by this sort of error. In the second case, the function that is the "legal" owner of the memory will find its stored values changed mysteriously and will misbehave as a result. In the third case, the heap management routines would probably get confused and start handing out wrong addresses. Errors of this kind are common in programs that use pointers heavily and are extremely difficult to find.

Susan was interested in this topic of errors in memory allocation, so we discussed it.

Susan: Can you give me an example of what an "invalid pointer" would be? Would it be an address in memory that is in use for something else rather than something that can be returned to the heap?

Steve: That's one kind of invalid pointer. The other type would be an address that doesn't exist at all; that is, one that is past the end of the possible legal addresses.

Susan: Oh, wait, so it would be returned to the heap but later if it is allocated to something else, it will cause just a tiny little problem because it is actually in use somewhere else?

Steve: You bet.

Susan: Is this a crash?

Steve: This is a major cause of crashes.

Susan: Oh yeah, this is cool, this is exciting. So this is what really happens when a crash occurs?

Steve: Do you know what a GPF (General Protection Fault) is? It's the operating system detecting the attempt by a program to access memory that doesn't belong to it.

Susan: I like this. So when there is no memory address allocated you get a error message?

Steve: Yes, or if it belongs to someone else. Of course, you'll be lucky to get anything other than a hard crash if it's a DOS program; at least in Windows you'll probably get a GPF instead.

Another way to go wrong with dynamic memory allocation is the opposite one. Instead of trying to delete something that was never dynamically allocated, you can forget to delete something that has been dynamically allocated. This is called a memory leak; it's very insidious, because the program appears to work correctly when tested casually. The usual way to find these errors is to notice that the program runs apparently correctly for a (possibly long) time and then fails due to running out of available memory. I should mention here how we can tell that we've run out of memory: The new operator returns a pointer with a 0 value. By convention, a pointer with the value 0 means "a pointer that doesn't point to anything". Hence, 0 is a reasonable value to return when there is no memory left. Our sample programs don't check the return value from new, so they will go along merrily as though they have actually allocated memory successfully. The result will be a spectacular failure when we actually try to use the "allocated" memory at location 0. Of course, a real program should check for this problem and tell the user that no more memory is left (if no better solution is available); , however, plenty of programs that have supposedly been working for a long time fail to check this situation sometimes.19 Given all of the ways to misuse dynamic memory allocation, we'll use it only when its benefits clearly outweigh the risks. To be exact, we'll restrict its use to controlled circumstances inside class implementations, to reduce the probability of such errors.

Susan had some questions about the idea of new returning a 0 if no memory is left.

Susan: Where is it written that new returns a value of 0?

Steve: That's what new does when it doesn't have anything to give you. By convention, 0 isn't a valid pointer value; therefore, getting a 0 from new means you're hosed.

Susan: How does a real program check for this?

Steve: By saying something like this:

p = new char[1000];

if (p == 0)

{

cout << "You're hosed!" << endl;

exit(1);

}

For your information, exit means "bail out of the program right now, without returning to the calling function, if any". The argument to exit is reported back to DOS as the return value from the program; 0 means OK, anything else means some sort of error.

The error prone nature of dynamic memory allocation is ironic, since it would be entirely possible for the library implementers (that is, the people who write the functions that are used by new and delete) to prevent or at least detect the problem of deleting something you haven't allocated or failing to delete something that you have allocated. After all, those routines handle all of the memory allocation and deallocation for a C++ program, so there's no reason that they couldn't keep track of what has been allocated and not released.

Of course, an ounce of prevention is worth a pound of cure, so avoiding these problems by proper design is the best solution. Luckily, it is possible to write programs so that this type of error is much less likely; basically, this approach requires keeping all dynamic memory allocation inside class implementations, rather than exposing it to the application programmer. We're following this approach with our string class, and it can also be applied to other situations where it is less straightforward. However, such applications are beyond the scope of this book.

Susan was intrigued by the possible results of forgetting to deallocate resources such as memory. Here's what resulted from her interest:

Susan: So when programs leak system resources, is that the result of just forgetting to delete something that is dynamically allocated?

Steve: Yes.

Susan: Then that would be basically a programming error or at least sloppiness on the part of the programmer?

Steve: Yes.

The Next Assignment

Having discussed some of the possible problems with dynamic allocation, let's continue with the code for operator = (Figure stringassignment). The next statement is m_Length = Str.m_Length;. This is the first time we've used the . operator to access a member variable of an object other than the object for which the member function was called. Up until now, we've been satisfied to refer to a member variable such as m_Length just by that simple name, as we would with a local or global variable. The name m_Length is called an unqualified name because it doesn't specify which object we're referring to. The expression m_Length by itself refers to the occurrence of the member variable m_Length in the object for which the current function was called; i.e., the string whose address is this (the string s in our example line s = n;). If you think about it, this is a good default, because member functions refer to member variables of their "own" object more than any other kinds of variables. Therefore, to reduce the amount of typing the programmer has to do, whenever we refer to a member variable without specifying the object to which it belongs, the compiler will assume that we mean the variable that belongs to the object for which the member variable was called (i.e, the one whose address is the current value of this). However, when we want to refer to a member variable of an object other than the one pointed to by this, we have to indicate which object we're referring to, which we do by using the . operator. This operator means that we want to access the member variable (or function) whose name is on the right of the . for the object whose name is on the left of the .. Hence, the expression Str.m_Length specifies that we're talking about the occurrence of m_Length that's in the variable Str, and the whole statement m_Length = Str.m_Length; means that we want to set the length of "our" string (i.e., the one pointed to by this) to the length of the argument string Str.

Susan had some questions about this issue of accessing variables of another string and how that relates to operator =.

Susan: I still don't get the . thingy.

Steve: All . does is separate the object (on the left) from the member variable or function (on the right). So s.operator=(n); might be roughly translated as "apply the operator = to the object s, with the argument n".

Susan: So wait: the . does more than separate; it allows access to other string member variables?

Steve: It separates an object's name from the particular variable or function that we're accessing for that object. In other words, Str.m_Length means "the instance of m_Length that is part of the object Str."

Susan: So in the statement m_Length = Str.m_Length; what we are doing is creating a new m_Length equal to the length of Str's m_Length for the = operator?

Steve: What we're doing is setting the value of the length (m_Length) for the string being assigned to (the left-hand string) to the same value as the length of the string being copied from (the right-hand string).

Susan: But it is going to be specific for this string?

Steve: If I understand your question, the value of m_Length will be set for the particular string that we're assigning a new value to.

Susan: When we say Str, does that mean that we are not using the variable pointed to by this? I am now officially lost.

Steve: Yes, that's what it means. In a member function, if we don't specify the variable we are talking about, it's the one pointed to by this; of course, if we do specify which variable we mean, then we get the one we specify.

Next, we use the statement m_Data = new char [m_Length]; to acquire the address of some memory that we will use to store our new copy of the data from Str; along with the address, new gives us the right to use that memory until we free it with delete.

Then we use memcpy to copy the data from Str (i.e., the group of characters starting at the address stored in Str.m_Data) to our newly allocated memory, which of course is pointed to by m_Data (i.e., the occurrence of m_Data in the string being assigned to). Now our target string is a fully independent entity with the same value as the string that was passed in.

Finally, we return *this, which means "the object to which this points", i.e., a reference to the string whose value we have just set, so that it can be used in further operations.

Although the individual statements weren't too much of a problem, Susan didn't get the big picture. Here's how I explained it to her:

Susan: I don't get this whole code thing for Figure stringassignment, now that I think about it. Why does this stuff make a new operator =? This is weird.

Steve: Well, what does operator = do? It makes the object on the left side have the same value as the object on the right side. In the case of a string, this means that the left-hand string should have the same length as the one on the right, and all the chars used to store the data for the right-hand string need to be copied to the address pointed to by m_Data for the left-hand string. That's what our custom = does.

Susan: Let's see. First we have to get some new memory for the new m_Data; then we have to make a copy. . . So then the entire purpose of writing a new operator = is to make sure that variables of that class can be made into separate entities when using the = sign rather than sharing the same memory address for their data?

Steve: Right.

Susan: I forget now why we did that.

Steve: We did it so that we could change the value of one of the variables without affecting the other one.

Before we move on to the next member function, I should mention that Susan and I had quite a lengthy correspondence about the notion of this. Here are the highlights of that discussion.

Susan: I don't understand this.

Steve: this refers to the object that a member function is being called for. For example, in the statement xyz.Read();, when the function named Read is called, the value of this will be the address of the object xyz.

Susan: OK, then, is this the result of calling a function? Or the address of the result?

Steve: Not quite either of those; this is the address of the object for which a class function is called.

Susan: Now that I have really paid attention to this and tried to commit it to memory it makes more sense. I think that what is so mysterious is that it is a hidden argument. When I think of an argument I think of something in (), as an input argument.

Steve: It actually is being passed as though it were specified in every call to a member function. The reason it is hidden is not to make it mysterious, but to reduce the amount of work the programmer has to do. Since almost every member function needs to access something via this, supplying it automatically is a serious convenience.

Susan: Now as far as my understanding of the meaning of this, it is the address of the object whose value is the result of calling a member function.

Steve: Almost exactly right; this is the address of the object for which a member function is called. Is this merely a semantic difference?

Susan: Not quite. Is there not a value to the object? Other than that we are speaking the same language. We usually do.

Steve: Yes, the object has a value. However, this is merely the address of the object, not its value.

Susan: How about writing this as if it were not hidden and was in the argument list; then show me how it would look. See what I mean? Show me what you think it would look like if you were to write it out and not hide it.

Steve: OK, that sounds good. Actually, I was thinking of doing that anyway.

Figure stringassignmentnothis shows what the code for operator = might look like if the this pointer weren't supplied automatically, both in the function declaration and as a qualifier for the member variable names.20

A hypothetical assignment operator (operator =) for the string class with explicit this (Figure stringassignmentnothis)

string& string::operator = (const string* this, const string& Str)

{

if (&Str != this)

{

delete [] this->m_Data;

this->m_Length = Str.m_Length;

this->m_Data = new char [this->m_Length];

memcpy(this->m_Data,Str.m_Data,this->m_Length);

}

return *this;

}

Note that every reference to a member variable of the current object would have to specify this. That would actually be more significant in writing the code than the fact that we would have to supply this in the call. Of course, how we would actually supply this when calling the operator = function is also a good question. Clearly the necessity of passing this explicitly would make for a messier syntax than just s = n;.

The Terminator

Now that we have seen how operator = works in detail, let's look at the next member function in the initial version of our string class, the destructor.21 A destructor is the opposite of a constructor; that is, it is responsible for deallocating any memory allocated by the constructor and performing whatever other functions have to be done before a variable dies. It's quite rare to call the destructor for a variable explicitly; as a rule, the destructor is called automatically when the variable goes out of scope. As we've seen, the most common way for this to happen is that a function returns to its calling function; at that time, destructors are called for all local variables that have destructors, whereas local variables that don't have destructors, such as those of native types, just disappear silently.22 Susan had some questions about how variables are allocated and deallocated. Here's the discussion that ensued.

Susan: I remember we talked about the stack pointer and how it refers to addresses in memory but I don't remember deallocating anything. What is that?

Steve: Deallocating variables on the stack merely means that the same memory locations can be reused for different local variables. Remember the function mess?

Susan: Oh, that is right, the data stays in the memory locations until the location is used by something else. It really isn't meaningful after it has been used unless it is initialized, is that right?

Steve: Yes, that's right.

Susan: When I first read about the destructor my reaction was, "well, what is the difference between this and delete?" But basically it just is a function that makes delete go into auto-pilot?

Steve: Basically correct.

Susan: How does it know you are done with the variable, so that it can put the memory back?

Steve: By definition, when the destructor is called, the variable is history. This happens automatically when it goes out of scope. For an auto variable, whether of native type or class type, this occurs at the end of the function where the variable was defined.

Susan: I don't understand this. I reread your explanation of "going out of scope" and it is unclear to me what is happening and what the alternatives are. How does a scope "disappear"?

Steve: The scope doesn't disappear, but the execution of the program leaves it. For example, when a function terminates, the local variables (which have local scope), go out of scope and disappear. That is, they no longer have memory locations assigned to them, until and unless the function starts execution again.

Susan: What if you need the variable again?

Steve: Then don't let it go out of scope.

Because destructors are almost always called automatically when a variable goes out of scope, rather than by an explicit statement written by the programmer, the only information guaranteed to be available to a destructor is the address of the variable to be destroyed. For this reason, the C++ language specifies that a destructor cannot have arguments. This in turn means that there can be only one destructor for any class, since there can be at most one function in a given class with a given name and the same type(s) of argument(s) (or, as in this case, no arguments).

As with the constructor(s), the destructor has a special name to identify it to the compiler. In this case, it's the name of the class with the token ~ (the tilde) prefixed to it, so the destructor for class string is named ~string.23 The declaration of this function is the next line in Figure stringinterface1, ~string();. Its implementation looks like Figure stringdestr.

The destructor for the string class (from code/string1.cc) (Figure stringdestr)

string::~string()

{

delete [] m_Data;

}

This function doesn't use any new constructs; we've already seen that the delete [] operator frees the memory allocated to the pointer variable it operates on.24 In this case, that variable is m_Data, which holds the address of the first one of the group of characters that make up the actual data contained by the string.

Now that we've covered nearly all of the member functions in the initial version of the string class, it's time for some review.

Review

We've almost finished building a concrete data type called the string class, which provides a means of storing and processing a group of characters similar to the facilities provided by a C string, but without some of the drawbacks of the latter data type. The fact that string is a concrete data type means that a string that is defined as a local variable in a function should be created when the function starts up and automatically deleted when the function ends. Also, we need to be able to copy a string to another string and have the two copies behave like independent variables, not linked together in the manner of Siamese twins.

The creation of an object is performed by a special member function called a constructor. Any class can have several constructors, one for each possible way that a newly created object can be initialized. So far, we've examined the interface and implementation of the default constructor, which takes no arguments, and a constructor that takes a char* argument. The former is needed to create a string that doesn't have an initial value, while the latter allows us to create a string that has the same contents as a C string. The default constructor is one of the required member functions in a concrete data type.

We've also seen that in the case of our string constructors, we need to know the order in which the member initialization expressions are executed; since this is dependent on the order of declaration of member variables, we have to make sure that those member variables are declared in the correct order for our member initialization expressions to work properly.

Continuing with the requirements for a concrete data type, we've implemented our own version of operator =, which can set one string to the same value as another string while leaving them independent of one another.

We've also created one other required member function for a concrete data type, the destructor, which is used to clean up after a string when it expires. This member function is called automatically for an auto variable at the end of the function where that variable is defined.

We're still short a copy constructor, which can create a string that has the same value as another pre-existing string. This may sound just like operator =, but it's not exactly the same. While operator = is used to set a string that already exists to the same value as another extant string, the copy constructor creates a brand-new string with the same value as one that already exists. We'll see how this works in the next chapter; in the meantime, let's take a look at some exercises intended to test your understanding of this material.

Exercises

1. What would happen if we compiled the program in Figure strex1? Why?

Exercise 1 (code\strex1.cc) (Figure strex1)

code/strex1.cc

2. What would happen if we compiled the program in Figure strex2? Why?

Exercise 2 (code\strex2.cc) (Figure strex2)

code/strex2.cc

3. What would happen if we compiled the program in Figure strex3? Why?

Exercise 3 (code\strex3.cc) (Figure strex3)

code/strex3.cc

4. What would happen if a user of our string class wrote an expression that tried to set a string variable to itself (e.g., a = a;) and we hadn't bothered to check for that situation in our operator = (Figure stringassignment).?

5. What would happen if we compiled the program in Figure strex5? Why?

Exercise 5 (code\strex5.cc) (Figure strex5)

code/strex5.cc

Conclusion

We've covered a lot of material about how a real, generally useful class such as string works in this chapter. In the next chapter, we'll continue with the saga of the string class, finishing up the additional functionality needed to turn it into a full-fledged concrete data type. We'll put this new functionality to the test in a modified version of the sorting algorithm from the early chapters that sorts strings rather than numeric values.

Answers to Exercises

1. The output of the compiler should look something like this:

strex1.cc: In function `int main()':

strex1.cc:21: member `m_Length' is a private member of class `string'

This one is simple; since m_Length is a private member variable of string, a nonmember function such as main can't access it.

2. The output of the compiler should look something like this:

strex2.cc: In function `int main()':

strex2.cc:9: constructor `string::string()' is private

strex2.cc:17: within this context

strex2.cc:17: in base initialization for class `string'

This is also pretty simple. Since the default constructor string::string() is in the private area, it's impossible for a nonmember function such as main to use it. Notice that there was no error message about string::string(char* p); that constructor is in the public area, so main is permitted to create a string from a C string. It's just the default constructor that's inaccessible.

3. The output of the compiler should look something like this:

strex3.cc:12: warning: `class string' only defines a private destructor and has no friends25strex3.cc: In function `int main()':

strex3.cc:16: destructor for type `string' is private in this scope

This answer is considerably less obvious than the previous ones. To be sure, the destructor is private and can't be called from main, but that doesn't explain why main is trying to call the destructor in the first place. The reason is that every auto variable of a type that has a destructor must have its destructor called at the end of that function. That's part of the mechanism that makes our objects act like "normal" variables, which also lose their values at the end of the function where they are declared.26 In the case of a user defined variable, though, more cleanup may be required; this is certainly true for strings, which have to deallocate the memory that they allocated to store their character data.

Therefore, you cannot create an object of a class whose destructor is private as an auto variable, as the automatic call of the destructor at the end of the scope would be illegal.

Susan didn't get this one exactly right, but she was obviously in the ballpark.

Susan: I have a note here that this program would not work because the ~string () thingy should be public and that, if this were to run, it would cause a memory leak. Am I on the right track?

Steve: Yes, you're close. Actually, what would happen is that the compiler would refuse to compile this program because it wouldn't be able to call ~string at the end of the function, since ~string is private. If the compiler were willing to compile this program without calling the destructor, there would indeed be a memory leak.

4. Let's take a look at the sequence of events that would have transpired if the user had typed a = a; and we hadn't taken the precaution of checking for that situation in the operator = code.27The first statement to be executed would be delete [] m_Data;. This gives the memory that had been allocated to store characters in string a back to the operating system.

The second statement to be executed would be m_Length = Str.m_Length;. Since m_Length and Str.m_Length are actually the same memory location in this case, this statement wouldn't do anything.

The third statement to be executed would be m_Data = new char [m_Length];. This would allocate memory for the target string, and assign it to the member variable m_Data.

The fourth statement to be executed would be memcpy(m_Data,Str.m_Data,m_Length);. This would copy m_Length bytes of data to the address stored in m_Data, which points to the newly allocated piece of memory, from the address stored in Str.m_Data, which points to. . . the same address. Remember, if this and &Str are the same, as they are in this case, then m_Data and Str.m_Data are two names for the same memory location. Therefore, this operation will have no effect. Furthermore, the preceding step has assigned the address of the newly allocated memory to m_Data, overwriting the previous contents of m_Data, that is, the address of the original contents of the string a. Therefore, the original value of a, which was pointed to by m_Data when we started, is no longer accessible. Even if we had a copy of that address, we couldn't use it because the memory to which it refers to has already been returned to the operating system and no longer belongs to us.

The net result of all of this is that the m_Data member variable of string a would point to uninitialized data.For a further explanation of this, you might want to take a look at the discussion I had with Susan. The topic is the if statement if (&Str != this), which checks whether the source string is the same as the destination string:

Susan: I don't get the purpose of this if statement. Do we want the two strings to be the same or to be different?

Steve: We want them to be the same after we do the copy; if they're the same before the copy, we don't need (or want) to do anything. This may not be completely obvious, so here's a more detailed explanation:

If the two strings are different when the if statement is executed, then we can safely execute the code controlled by the if statement, because executing delete on the old contents of the string on the left-hand side of the = sign won't affect the string on the right-hand side.

If the two strings are actually the same string, then the delete call will deallocate the memory that was used to hold the string's data; therefore, the memcpy will be copying from a pointer that is no longer valid. Even if this code would work, there's no reason to execute it if the two strings are already the same at the beginning of the code for the assignment statement.

Susan: OK, so if the two strings are the same then the if statement won't go into effect, but if they are different, then it will; it is important that they are different otherwise you would be deleting the original address of the caller?

Steve: Right, with one very small modification: It's the data at the original address of the string that would get freed prematurely.

5. This one was a little tricky. I'll bet you thought that making the default constructor private would keep this from compiling, but it turns out that we're not using the default constructor. That should be obvious in the line string n("Test");, which clearly uses string::string(char* p), but what does the compiler do with the line string x = n;? You might think that it calls the default constructor to make x and then uses operator = to copy the value of n into it. If that were true, the private status of the default constructor would prevent the program from compiling. However, what actually happens is that the copy constructor string::string(const string&) is used to make a brand new string called x with the same value as n. So, in this case, the private access specifier on the default constructor doesn't get in the way.

Footnotes

  1. By the way, char* can also be written as char *, but I find it clearer to attach the * to the data type being pointed to.
  2. C programmers are likely to object that a pointer has some properties that differ from those of a memory address. Technically, they're right, but in the specific case of char* the differences between a pointer and a memory address will never matter to us. Since we aren't going to be using any other types of pointers in this book, we can avoid adding yet another source of complexity to this discussion.
  3. As this implies, it's possible to have a pointer to any type of variable, not just to a char. For example, a pointer to a short would have the type short*, and similarly for pointers to any other data type. However, we won't need to use pointers to any other type of variables in this book.
  4. This terminology doesn't exactly match the official nomenclature used by Bjarne Stroustrup to describe dynamic memory allocation. However, every C and C++ programmer will understand you if you talk about dynamic storage, and I believe that this terminology is easier to understand than the official terminology.
  5. The reason for the different numbers of digits in the representations of m_Length, m_Data, and the data for the null C string is to indicate how long those data items are.
  6. I know we've skipped the copy constructor, the assignment operator, and the destructor. Don't worry, we'll get to them later.
  7. There's nothing magical about the name p for a pointer. You could call it George if you wanted to, but it would just confuse people. The letter p is often used for pointers, especially by programmers who can't type, which unfortunately is fairly common.
  8. This is probably a good place to clear up any confusion you might have about whether there are native and user defined functions; there is no such distinction. However, the reason might not be what you expect; rather than there being no user defined functions, it's just the opposite. That is, functions are never native in the way that variables are: built into the language. Quite a few functions such as strlen and memcpy come with the language; that is, they are supplied in the standard libraries that you get when you buy the compiler. However, these functions are not privileged relative to the functions you can write yourself, unlike the case with native variables in C. In other words, you can write a function in C or C++ that looks and behaves exactly like one in the library, whereas it's impossible in C to add a type of variable that has the same appearance and behavior as the native types; the knowledge of the native variable types is built into the C compiler and cannot be changed by the programmer.

    But why aren't there any native functions? Because the language was designed to be easy to move (or port) from one machine to another. This is easier if the compiler is simpler; hence, most of the functionality of the language is provided by functions that can be written in the "base language" the compiler knows about. This includes basic functions such as strlen and memcpy, which can be written in C. For purposes of performance, they are often written in assembly language instead, but that's not necessary to get the language running on a new machine.

  9. You may wonder why we even need to include the null byte if we're going to store the length also. Isn't this redundant? Yes, it is, but if we want to be able to use the gdb debugger to look at our strings, we have to include that null byte at the end. Otherwise, gdb won't know how to display them in an intelligible format.
  10. This is a bit of an oversimplification. In a "real" operating system, the heap will occupy most or all of the free memory in your computer. However, in MS-DOS, this is not true; the problem is that MS-DOS was devised to run on a very limited processor called the 8088, which had a maximum of 640K of available memory for programs. As a result of this historical limitation, a program that runs under MS-DOS can't access most of the memory in today's machines. Even if you have 32 MB of RAM, you'll run out of memory if you try to allocate more than a few hundred KB from the heap under MS-DOS. You'll be happy to know that the programs you compile with the compiler on the CD-ROM in the back of this book don't have this limitation, as they run under a "DOS extender", an operating system extension which was invented largely to solve this problem.
  11. By the way, in case you're wondering what useful function this statement serves in the sample program, the answer is none. It's just to illustrate how operator = works.
  12. As this explanation may suggest, we can't make up our own operators with strange names by prefixing those names with operator; we're limited to those operators that already exist in the C++ language.
  13. In this section, you're going to see a lot of hedging of the form "in this context, x means y". The reason is that C and C++ both reuse keywords and symbols in many different situations, often with different meanings in each situation. In my opinion, this is a flaw in the design of these languages, as it makes learning them more difficult. The reason for this reuse is that every time you add a keyword, it's possible that formerly working code will break as a result. Personally, I think this is an overrated problem compared to the problems caused by overuse of the same keywords; however, I don't have a lot of old C or C++ code to maintain, so maybe I'm biased.
  14. A token is the smallest part of a program that the compiler treats as a separate unit; it's analogous to a word in English, with a statement being more like a sentence. For example, string is a token, as are :: and (. On the other hand, x = 5; is a statement.
  15. For the C++ purist, I should mention that this applies to all the member functions in this book. There's another kind of member function that doesn't get a this pointer passed to it, but we won't get to discuss that other type.
  16. Or any other constructor that allocates memory in which to store characters. I'm just referring to the char* constructor because we've already analyzed that one.
  17. By the way, this is one of the previously mentioned times when we have to explicitly deal with the difference between a pointer used as "the address of an item" and one used as "the address of some number of items"; the [] after delete tells the compiler that the latter is the current situation. The C++ standard specifies that any memory that was allocated via a new expression containing [] must be deleted via delete [].
  18. There's an exception to this rule: Calling delete for a pointer with the value 0 will not cause any untoward effects, as such a pointer is recognized as "pointing to nowhere".
  19. The forthcoming standard for the C++ language will change the behavior of new when no more memory is left. Instead of returning 0, new will notify the application programmer via a mechanism called an exception. Unfortunately, this mechanism is outside the scope of this book.
  20. By the way, I've introduced another new notation here: the operator ->, which is the "object pointer member access" operator. It separates a pointer to an object name, on its left, from the member variable or member function on its right. In other words, -> does the same thing for pointer variables that . does for objects. That is, if the token on the right of -> is a member variable, that token refers to the specific member variable belonging to the object pointed to by the pointer on the left of ->; if the token on the right of -> is a member function, then it is called for the object pointed to by the pointer on the left of ->. For example, this->m_Data means "the m_Data that belongs to the object pointed to by this".
  21. Susan has suggested that the constructor, the destructor, and the assignment operator would make good action figures: perhaps Saturday morning cartoons would be a good outlet for getting children interested in programming?
  22. If we use new to allocate memory for a variable that has a destructor, then the destructor is called when that variable is freed by delete. However, we won't be using new to create class objects in this book.
  23. In case you're wondering, this somewhat obscure notation was chosen because the tilde is used to indicate logical negation; that is, if some expression x has the logical value true, then ~x will have the logical value false, and vice-versa.
  24. By the way, in case you were wondering what happened to the old values of the m_Data and m_Length member variables, we don't have to worry about those because the string being destroyed won't ever be used again.
  25. In case you're wondering what a friend is, it's a class or a function that has special access to the internal workings of another class. We'll get into that mechanism in Chapter downpath.htm.
  26. To be more precise, the destructor is called at the end of the scope in which the variable was defined. It's possible for a variable to have scope smaller than an entire function; in that case, the variable is destroyed when its scope expires.
  27. See Figure stringassignment for the code.

Return to the table of contents